package puzzle.core;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.Serializable;
import java.util.List;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.newdawn.slick.loading.DeferredResource;
import org.newdawn.slick.loading.LoadingList;
import puzzle.core.Direction;
import puzzle.core.LevelData;
import puzzle.core.Tile;
import puzzle.core.TileFactory;
import puzzle.core.TileType;
import puzzle.core.tiles.BoostTile;
import puzzle.core.tiles.PortTile;
import puzzle.util.MapStream;
import puzzle.util.ResourceManager;

/**
 * LevelData is the container for a LevelMap. It 
 * contains the 2D array of tiles.
 */
public class LevelData implements Serializable, DeferredResource {
    
  /** The tile indices of the TileTypes. */
  private Tile[][] array = null;
  
  private int startX = 0;
  private int startY = 0;
  
  private String name = "";
  private String author = "";
  private String description = "";
  
  private InputStream in = null;
  private TileFactory tf = null;
  private boolean loaded = false;
  public String path = null;
  
  public LevelData() {
    array = new Tile[LevelMap.WIDTH][LevelMap.HEIGHT];
    loaded = true;
  }
    
  public LevelData(InputStream in, TileFactory tf) throws IOException {
    this();
    this.in = in;
    this.tf = tf;
    this.loaded = false;
    
    if (!ResourceManager.isDeferredLoading())
      load();
    else 
      ResourceManager.getLoadingList().add(this);
  }
  
  public LevelData(InputStream in) throws IOException {
    this(in, TileFactory.getCurrentInstance());
  }
  
  public void toXML(OutputStream out) throws IOException {
    OutputStreamWriter w = new OutputStreamWriter(out);
    
    Document document = DocumentHelper.createDocument();
        
    Element root = document.addElement("map-data")
                                .addAttribute("version", MapStream.VERSION);
        
    if (name!=null)
      root.addElement("name").setText(name);
    if (author!=null)
      root.addElement("author").setText(author);
    if (description!=null)
      root.addElement("description").setText(description);
    
    for (int x=0; x<LevelMap.WIDTH; x++) {
      Element arrayElement = root.addElement("tile-array");
      for (int y=0; y<LevelMap.HEIGHT; y++) {
        Tile t = array[x][y];
        Element tileElement = arrayElement.addElement("tile");
        
        TileType type = t.getTileType();
        
        Direction dir = null;
        Direction linkDir = null;
        int lx=0, ly=0;
        boolean linkable = false;
        
        if (t.getTileType()==TileType.BOOST) {
          dir = ((BoostTile)t).getDirection();
        }
        else if (t.getTileType()==TileType.PORT) {
          PortTile port = (PortTile)t;
          
          dir = port.getDirection();
          linkDir = port.getLinkDirection();
          lx = port.getLinkX();
          ly = port.getLinkY();
          linkable = true;
        }
        
        tileElement.addElement("type").setText(type.toString());
        
        if (dir!=null) {
          tileElement.addElement("direction").setText(dir.toString());
        }
        if (linkable) {
          Element linkElement = tileElement.addElement("link");
          linkElement.addElement("x").setText(String.valueOf(lx));
          linkElement.addElement("y").setText(String.valueOf(ly));
          linkElement.addElement("direction").setText(linkDir.toString());
        }
      }
    }
    
    //no special output format since it gets compressed anyways
    document.write(w);
    w.close();
    out.close();
  }
    
  /** Places the specified tile in the level. */
  public void putTile(Tile e, int x, int y) {
    if (e.getTileType()==TileType.START) {
      setStart(x, y);
    }
    array[x][y] = e;
  }
    
  /** Gets an entity at the specified tile. */
  public Tile getTile(int x, int y) {
    return array[x][y];
  }
  
  public int getStartX() {
    return startX;
  }
  
  public int getStartY() {
    return startY;
  }
  
  public void setStart(int x, int y) {
    startX = x;
    startY = y;
  }
  
  public String getName() {
    return name;
  }
  
  public String getAuthor() {
    return author;
  }
  
  public String getDescription() {
    return description;
  }
  
  public void setAuthor(String s) {
    this.author = s;
  }
  
  public void setName(String s) {
    this.name = s;
  }
  
  public void setDescription(String s) {
    this.description = s;
  }
  
  public boolean isEmpty() {
    for (int x=0; x<LevelMap.WIDTH; x++) {
      for (int y=0; y<LevelMap.HEIGHT; y++) {
        Tile t = array[x][y];
        if (t!=null && t.getTileType()!=TileType.GROUND) {
          return false;
        }
      }
    }
    return true;
  }
  
  @SuppressWarnings("unchecked")
  public void load() throws IOException {
    /*
      --- FORMAT FOR MAP DATA FILE:
      <map-data>
        <name>Zero Hour</name>
        <author>Matt</name>
        <description>A simple map.</description>
        <tile-array>
          <tile>
            <type>PORT</type>
            <direction>UP</direction>
            <link>
              <x>5</x>
              <y>4</y>
              <direction>UP</direction>
            </link>
          </tile>
          <tile>
            <type>GROUND</type>
          </tile>
          <tile>
            <type>BOOST</type>
            <direction>DOWN</direction>
          </tile>
        </tile-array>
      </map-data>
    */
    
    SAXReader reader = new SAXReader();
    Document document = null;
    try {
      document = reader.read(in);
    }
    catch (Exception e) {
      throw new IOException("Error loading xml file: "+e.getMessage());
    }
    
    Element root = document.getRootElement();
    
    if (root==null) {
      throw new IOException("No <map-data> element found!");
    }
    
    String nname = root.elementText("name");
    this.name = (nname==null) ? name : nname;
    
    String nauthor = root.elementText("author");
    this.author = (nauthor==null) ? author : nauthor;
    
    String ndesc = root.elementText("description");
    this.description = (ndesc==null) ? description : ndesc;
    
    //a list of all tile-array elements
    List<Element> list1 = (List<Element>)root.elements("tile-array");
    
    for (int x=0; x<LevelMap.WIDTH && x<list1.size(); x++) {
      Element e = list1.get(x);
        
      //a list of all tile elements in this array
      List<Element> list2 = (List<Element>)e.elements("tile");
      for (int y=0; y<LevelMap.HEIGHT && y<list2.size(); y++) {
        Element tileElement = list2.get(y);
                
        String typeStr = tileElement.element("type").getText();
        TileType type = TileType.valueOf(typeStr);
        
        Element dirElement = tileElement.element("direction");
        Direction dir = null;
        if (dirElement!=null) {
          dir = Direction.valueOf(dirElement.getText());
        }
        
        Element linkElement = tileElement.element("link");
        int lx = 0;
        int ly = 0;
        Direction ldir = Direction.UP;
        
        if (linkElement!=null) {
          try {
            Element xel = linkElement.element("x");
            String xstr = xel.getText();
            lx = Integer.parseInt(xstr);
          
            Element yel = linkElement.element("y");
            String ystr = yel.getText();
            ly = Integer.parseInt(ystr);
          
            Element direl = linkElement.element("direction");
            ldir = Direction.valueOf(direl.getText());
          }
          catch (Exception exc) {
            System.err.println("WARNING: Error parsing linkable at "+x+", "+y);
          }
        }
        
        Tile tile = null;
        
        switch (type) {
          case START:
            tile = tf.getStart();
            break;
          case END:
            tile = tf.getEnd();
            break;
          case WALL:
            tile = tf.getWall();
            break;
          case GROUND:
            tile = tf.getGround();
            break;
          case BOOST:
            tile = tf.getBoost(dir);
            break;
          case PORT:
            tile = tf.getPort(dir);
            ((PortTile)tile).setLinkAt(lx, ly);
            ((PortTile)tile).setLinkDirection(ldir);
            break;
        }
        
        putTile(tile, x, y);
      } //end array loop
    } //end loop
    
    try { in.close(); } catch( Exception e ){}
    
    loaded = true;
  }
}